%%%-------------------------------------------------------------------
%% @doc crontab_trigger public API
%% @end
%%%-------------------------------------------------------------------

-module(crontab_trigger).
-behaviour(gen_server).


%% --------------------------------------------------------------------
%% External exports
%% --------------------------------------------------------------------
-export([reload/1, show/0]).
-export([start_link/1]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).

-record(state, {files, items}).
-record(cron_item, {minute, hour, day, month, week, mfa}).

%% ====================================================================
%% External functions
%% ====================================================================
reload(Files) ->
    gen_server:call(?MODULE, {reload, Files}).

show() ->
    gen_server:call(?MODULE, show).

start_link(Files) ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, Files, []).


%% ====================================================================
init(Files) ->
    Items = parse(Files),
    % io:format("~ncrontab started :~n"),
    % io:format("  -- crontab file amount: ~p~n", [length(Files)]),
    % io:format("  -- crontab itme amount: ~p~n", [length(Items)]),
    {ok, #state{files = Files, items = Items}}.

%% --------------------------------------------------------------------
handle_call(show, _From, State) ->
    io:format("~ncrontab info:~n"),
    io:format("  -- crontab file amount: ~p~n", [length(State#state.files)]),
    [io:format("   + ~p~n", [F]) || F <- State#state.files],
    io:format("  -- crontab itme amount: ~p~n", [length(State#state.items)]),
    [io:format("   + ~p~n", [I]) || I <- State#state.items],
    {reply, ok, State};

handle_call({reload, Files}, _From, State) ->
    try 
        Items = parse(Files),
        {reply, {"reload succ"}, #state{files = Files, items = Items}}
    catch
        throw:Err      -> {reply, {"reload fail", Err}, State};
        Class:Reason:_ -> {reply, {"reload fail", Class, Reason}, State}
    end;

handle_call(_Info, _From, State) ->
    {reply, ok, State}.

%% --------------------------------------------------------------------
handle_cast({timercall, {_Year, Month, Day}, {Hour, Minute, _Second}, Week}, State) ->
    check(Minute, Hour, Day, Month, Week, State#state.items),
    {noreply, State};

handle_cast(_Info, State) ->
    {noreply, State}.

%% --------------------------------------------------------------------
handle_info(_Info, State) ->
    {noreply, State}.

%% --------------------------------------------------------------------
terminate(_Reason, _Status) ->
    ok.

%% --------------------------------------------------------------------
code_change(_oldvsn, Status, _extra) ->
    {ok, Status}.


%% internal functions
%% --------------------------------------------------------------------
%%-spec parse(Files) -> [#cron_item{}]
parse(Files) ->
    lists:flatten( [parse_file(File) || File <- Files] ).

parse_file(File) ->
    try 
        case file:consult(File) of
            {error, enoent} ->  throw({"crontab file noexist", File});
            {error, Err} ->     throw({"crontab file format err", File, file:format_error(Err)});
            {ok, Crontabs} ->   [parse_item(Cron) || Cron <- Crontabs]
        end
    catch
        throw:Error ->          throw({File, Error});
        Class:Reason:_ ->       throw({File, Class, Reason})
    end.

parse_item({{Minute, Hour, Day, Month, Week}, {M, F, A}}) when is_atom(M), is_atom(F), is_list(A) ->
    #cron_item{ minute = parse_field(Minute, 0, 59)
              , hour   = parse_field(Hour,   0, 23)
              , day    = parse_field(Day,    1, 31)
              , month  = parse_field(Month,  1, 12)
              , week   = parse_field(Week,   1, 7)
              , mfa    = {M, F, A}
              };
parse_item(Cron) -> throw({"crontab format err", Cron}).


% "*", "3", "2-5/2", "2-6", "*/3", "1,3-5,6-31/7"
parse_field("*",   _Min, _Max) -> any;
parse_field(Field,  Min,  Max) when is_integer(Field), Field >= Min, Field =< Max -> [Field];
parse_field(Field, _Min, _Max) when is_integer(Field) -> throw("out of range");
parse_field(Field,  Min,  Max) ->
    lists:flatten( [parse_field_range(F, Min, Max) || F <- string:tokens(Field, ",")] ).

parse_field_range(Field, Min, Max) ->
    case string:tokens(Field, "/") of
        ["*",   Inc] -> lists:seq(Min, Max, list_to_integer(Inc));
        [Range, Inc] -> parse_field_range(Range, Min, Max, list_to_integer(Inc));
        [Range]      -> parse_field_range(Range, Min, Max, 1)
    end.

parse_field_range(Range, Min, Max, Inc) ->
    case string:tokens(Range, "-") of
        [SF, SL] ->
            F = list_to_integer(SF),
            L = list_to_integer(SL),
            if
                F < Min -> throw("min out of range");
                L > Max -> throw("max out of range");
                true -> lists:seq(F, L, Inc)
            end;
        [Num] -> list_to_integer(Num);
        _ -> throw("format err")
    end.


%% --------------------------------------------------------------------
%% check and run item
check(Minute, Hour, Day, Month, Week, [Item | Items]) ->
    try 
        true = check_field(Minute, Item#cron_item.minute),
        true = check_field(Hour,   Item#cron_item.hour),
        true = check_field(Day,    Item#cron_item.day),
        true = check_field(Month,  Item#cron_item.month),
        true = check_field(Week,   Item#cron_item.week),
        {M, F, A} = Item#cron_item.mfa,
        erlang:spawn(M, F, A)
    catch
        _:_:_ -> ignore
    end,
    check(Minute, Hour, Day, Month, Week, Items);
check(_Minute, _Hour, _Day, _Month, _Week, []) -> ok.

check_field(_T, any) -> true;
check_field(T, List) -> lists:member(T, List).





